今天的內容將延續 上一篇 文章中 Operation Process 裡的 3. ,同時我們會非常深入地去解析實現整個 transform
function 的程式碼邏輯,對 Operation 的運作流程還不熟悉的讀者們,筆者建議先回頭看完上一篇的內容後再接著繼續閱讀本篇的內容。
我們在 Day18 介紹 Slate 如何使用 Immer.js 時有先簡單提到這個 transform
function 過,再丟一次 code 的內容幫讀者回憶一下:
export const GeneralTransforms: GeneralTransforms = {
/**
* Transform the editor by an operation.
*/
transform(editor: Editor, op: Operation): void {
editor.children = createDraft(editor.children)
let selection = editor.selection && createDraft(editor.selection)
try {
selection = applyToDraft(editor, selection, op)
} finally {
editor.children = finishDraft(editor.children)
if (selection) {
editor.selection = isDraft(selection)
? (finishDraft(selection) as Range)
: selection
} else {
editor.selection = null
}
}
},
}
transform
function 的內容存放在 transforms/general.ts 這個 file 裡面,跟其他 Transform methods 一起放在同一個 transforms/directory 底下。
這個 method 做的事情本身不多,主要就是將 editor
底下的 children
與 selection
丟入 Immer 的 createDraft
製作 Draft-State ,並在整個運算結束以後執行 finishDraft
而已。
主要的運算工作都是透過 applyToDraft
來執行,所以 ... 是的!我們今天會把所有的精力都放在這個 function 上,那麼以下就~正文開始!
applyToDraft
這個 function 基本上就是由一連串的 Switch case
所組成不同的 Operation type 執行不同的運算內容,並在最後回傳計算完的 selection
。
我們就照著圖片上的順序一一介紹吧!
insert_node
『插入節點』
先判斷欲 insert 的 node 路徑的最後一個值是否大於它的 parent node 的 children 數量,若大於則代表此路徑超過了同一層 nodes 最尾端的 index ,因而是個不合法的操作
const { path, node } = op
const parent = Node.parent(editor, path)
const index = path[path.length - 1]
if (index > parent.children.length) {
throw new Error(
`Cannot apply an "insert_node" operation at path [${path}] because the destination is past the end of the node.`
)
}
若小於或等於則直接 insert 進指定的位置:
parent.children.splice(index, 0, node)
接著透過 Range.points
method Iterate anchor
與 focus
point ,再丟入 Point.transform
轉換:
if (selection) {
for (const [point, key] of Range.points(selection)) {
selection[key] = Point.transform(point, op)!
}
}
insert_text
『插入文字節點』
如果要被 insert 的 text
為空值則直接 break
此次的 transform
const { path, offset, text } = op
if (text.length === 0) break
將 path
丟入 Node.leaf
method 確保它是一個合法的 Text node
const node = Node.leaf(editor, path)
/** 順便附上 Node.leaf method 內容 */
// node.ts
/**
* Get the node at a specific path, ensuring it's a leaf text node.
*/
leaf(root: Node, path: Path): Text {
const node = Node.get(root, path)
if (!Text.isText(node)) {
throw new Error(
`Cannot get the leaf node at path [${path}] because it refers to a non-leaf node: ${node}`
)
}
return node
}
結束事前的判斷程序以後再將字串組在一起 &賦值
const before = node.text.slice(0, offset)
const after = node.text.slice(offset)
node.text = before + text + after
最後跟 insertNode
一樣對 selection
做一模一樣的操作,程式碼是一樣的我們就直接略過了。
merge_node
『合併節點』
它的做法是將 operation 裡給定的 path
指向的 node 與它的『前一個 sibling node 合併』。
首先取得 path 指向的 node 資料、前一個 sibling node 資料、 parent node 資料以及 path 指向的 node 的 index
const { path } = op
const node = Node.get(editor, path)
const prevPath = Path.previous(path)
const prev = Node.get(editor, prevPath)
const parent = Node.parent(editor, path)
const index = path[path.length - 1]
接著判斷它們可否合併,只有『同時為 Text node 』與『同時不為 Text node 』這兩種情形可以進行合併。前者將兩組字串合併在一起,後者則是將兩個 Node 的 children
合併在一起
if (Text.isText(node) && Text.isText(prev)) {
prev.text += node.text
} else if (!Text.isText(node) && !Text.isText(prev)) {
prev.children.push(...node.children)
} else {
throw new Error(
`Cannot apply a "merge_node" operation at path [${path}] to nodes of different interfaces: ${node} ${prev}`
)
}
然後再拔掉 path 指向的 node value
parent.children.splice(index, 1)
最後跟 insertNode
一樣對 selection
做一模一樣的操作,程式碼是一樣的我們就直接略過了。
move_node
『移動節點』
與字面上的意思一樣,做的事情就是將節點舊的 path 移動到新的 path 。
首先先避開『舊路徑為新路徑的祖先』這個可能性並取得節點、父節點、 index 等資料:
const { path, newPath } = op
if (Path.isAncestor(path, newPath)) {
throw new Error(
`Cannot move a path [${path}] to new path [${newPath}] because the destination is inside itself.`
)
}
const node = Node.get(editor, path)
const parent = Node.parent(editor, path)
const index = path[path.length - 1]
接著是更新節點資料的部分,因為在異動了原始的節點資料後會導致傳入的 path 資料過期而變得不可用,所以這邊的做法是取得 transform 後的新 path 以後再取得這個新 path 的父節點以及新的 index
資料,然後再對這些取得的新資料進行操作,而不是直接操作傳入的 newPath
資料
// This is tricky, but since the `path` and `newPath` both refer to
// the same snapshot in time, there's a mismatch. After either
// removing the original position, the second step's path can be out
// of date. So instead of using the `op.newPath` directly, we
// transform `op.path` to ascertain what the `newPath` would be after
// the operation was applied.
parent.children.splice(index, 1)
const truePath = Path.transform(path, op)!
const newParent = Node.get(editor, Path.parent(truePath)) as Ancestor
const newIndex = truePath[truePath.length - 1]
newParent.children.splice(newIndex, 0, node)
最後跟 insertNode
一樣對 selection
做一模一樣的操作,程式碼是一樣的我們就直接略過了。
remove_node
『刪除節點』
刪除節點本身的操作非常的基本,就是直接透過 Array 的 splice
method 來達成而已:
const { path } = op
const index = path[path.length - 1]
const parent = Node.parent(editor, path)
parent.children.splice(index, 1)
主要的內容都集中在處理 selection
的更新上,因為刪除的節點有可能是 selection
裡的 anchor
或 focus
point 。
這邊的作法是:
selection
裡的 anchor
與 focus
pointop.path
『之前』或是『之後』的文字節點selection
point 更新為該節點的最後一個字,如果是『之後』的就更新為該節點的第一個字,都沒找到就直接將 selection
設為 null
// Transform all of the points in the value, but if the point was in the
// node that was removed we need to update the range or remove it.
if (selection) {
for (const [point, key] of Range.points(selection)) {
const result = Point.transform(point, op)
if (selection != null && result != null) {
selection[key] = result
} else {
let prev: NodeEntry<Text> | undefined
let next: NodeEntry<Text> | undefined
for (const [n, p] of Node.texts(editor)) {
if (Path.compare(p, path) === -1) {
prev = [n, p]
} else {
next = [n, p]
break
}
}
if (prev) {
point.path = prev[1]
point.offset = prev[0].text.length
} else if (next) {
point.path = next[1]
point.offset = 0
} else {
selection = null
}
}
}
}
remove_text
『移除節點內的文字』
因為是操作同一個節點內的文本內容所以很基本,就是取得字串後組合而已。
const { path, offset, text } = op
if (text.length === 0) break
const node = Node.leaf(editor, path)
const before = node.text.slice(0, offset)
const after = node.text.slice(offset + text.length)
node.text = before + after
最後跟 insertNode
一樣對 selection
做一模一樣的操作,程式碼是一樣的我們就直接略過了
set_node
『設定節點屬性』
它會擋掉對 root node 的節點屬性設定、對 children
或 text
等主要資料的設定
const { path, properties, newProperties } = op
if (path.length === 0) {
throw new Error(`Cannot set properties on the root node!`)
}
const node = Node.get(editor, path)
for (const key in newProperties) {
if (key === 'children' || key === 'text') {
throw new Error(`Cannot set the "${key}" property of nodes!`)
}
// ...
}
剩下的就是實作邏輯了, 屬性 value
為 null
,或是原本有這項屬性但更新後卻沒有的話,會刪掉節點裡的這項屬性,否則會直接賦值到指定的屬性上
for (const key in newProperties) {
// ...
const value = newProperties[key]
if (value == null) {
delete node[key]
} else {
node[key] = value
}
}
// properties that were previously defined, but are now missing, must be deleted
for (const key in properties) {
if (!newProperties.hasOwnProperty(key)) {
delete node[key]
}
}
set_selection
『設定 selection 屬性』
功能很直觀,裡頭的程式碼主要也都是處理一些 edge cases ,例如: selection
原本為 null
的話,新設定的屬性內容必須符合一個合法的 Range
type 該有的 properties ,以及不能將 anchor
、 focus
的 value 設為 null
等等
case 'set_selection': {
const { newProperties } = op
if (newProperties == null) {
selection = newProperties
} else {
if (selection == null) {
if (!Range.isRange(newProperties)) {
throw new Error(
`Cannot apply an incomplete "set_selection" operation properties ${JSON.stringify(
newProperties
)} when there is no current selection.`
)
}
selection = { ...newProperties }
}
for (const key in newProperties) {
const value = newProperties[key]
if (value == null) {
if (key === 'anchor' || key === 'focus') {
throw new Error(`Cannot remove the "${key}" selection property`)
}
delete selection[key]
} else {
selection[key] = value
}
}
}
break
}
split_node
『拆分節點』
就是一個將節點一分為二的功能,唯一的限制是不能對 Editor
做拆分
const { path, position, properties } = op
if (path.length === 0) {
throw new Error(
`Cannot apply a "split_node" operation at path [${path}] because the root node cannot be split.`
)
}
將基本的資料取出來設為變數以後,會接著區分出 Text
node 與 Element
node 。
前者會對節點內的字串做操作:
const node = Node.get(editor, path)
const parent = Node.parent(editor, path)
const index = path[path.length - 1]
let newNode: Descendant
if (Text.isText(node)) {
const before = node.text.slice(0, position)
const after = node.text.slice(position)
node.text = before
newNode = {
...(properties as Partial<Text>),
text: after,
}
}
後者則是對 children
node 做操作:
else {
const before = node.children.slice(0, position)
const after = node.children.slice(position)
node.children = before
newNode = {
...(properties as Partial<Element>),
children: after,
}
}
然後將新生成的 newNode
塞進對應的位置:
parent.children.splice(index + 1, 0, newNode)
最後跟 insertNode
一樣對 selection
做一模一樣的操作,程式碼是一樣的我們就直接略過了。
呼~一個接著一個介紹,總算是迎來尾聲了。
雖說這篇介紹的 transform
function 是 Slate 主要用於處理 Slate node tree 的資料更新的,但我們同時也能發現其實還是有部分的邏輯是被拆到其他的 function 去處理的。
也就是針對『 Location types 』更新的內容,只要是諸如 selection
的 point 更新,或是 path 更新等等的功能都是交由各自對應到的 type 的 transform
method api 來處理( Point.transform
、 Path.transform
)
下一篇我們就會將目光聚焦在這些 methods 上,來看看在這裡頭又是如何更新 Location type 的內容的。
明天見各位~